#!/usr/bin/env python3

import xml.etree.ElementTree as ET
import sys
import os
import requests
import tempfile
import shutil
import argparse
import random
import dnf
import re
import logging
from packaging.version import parse as ver_parse

user_agent = [
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
"Mozilla/5.0 (Windows NT 10.0; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0",
"Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; InfoPath.3; rv:11.0) like Gecko",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)",
"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
"Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
"Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.8.131 Version/11.11",
"Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.11",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Maxthon 2.0)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; TencentTraveler 4.0)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; The World)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SE 2.X MetaSr 1.0; SE 2.X MetaSr 1.0; .NET CLR 2.0.50727; SE 2.X MetaSr 1.0)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; 360SE)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Avant Browser)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)",
"Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5",
"Mozilla/5.0 (iPod; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5",
"Mozilla/5.0 (iPad; U; CPU OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5",
"Mozilla/5.0 (Linux; U; Android 2.3.7; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"MQQBrowser/26 Mozilla/5.0 (Linux; U; Android 2.3.7; zh-cn; MB200 Build/GRJ22; CyanogenMod-7) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"Opera/9.80 (Android 2.3.4; Linux; Opera Mobi/build-1107180945; U; en-GB) Presto/2.8.149 Version/11.10",
"Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13",
"Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; en) AppleWebKit/534.1+ (KHTML, like Gecko) Version/6.0.0.337 Mobile Safari/534.1+",
"Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.0; U; en-US) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/233.70 Safari/534.6 TouchPad/1.0",
"Mozilla/5.0 (SymbianOS/9.4; Series60/5.0 NokiaN97-1/20.0.019; Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebKit/525 (KHTML, like Gecko) BrowserNG/7.1.18124",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0; HTC; Titan)",
"UCWEB7.0.2.37/28/999",
"NOKIA5700/ UCWEB7.0.2.37/28/999",
"Openwave/ UCWEB7.0.2.37/28/999",
"Mozilla/4.0 (compatible; MSIE 6.0; ) Opera/UCWEB7.0.2.37/28/999",
]

ns = {'http://www.icasi.org/CVRF/schema/cvrf/1.1',
      'http://www.icasi.org/CVRF/schema/prod/1.1',
      'http://www.icasi.org/CVRF/schema/vuln/1.1'
     }

euler_cvrf_url = 'https://mirrors.163.com/openeuler/security/data/cvrf'

def download(url, dest):
    if not os.path.exists(dest):
        os.makedirs(dest)
    b = os.path.basename(url)
    path = "%s/%s" %(dest, b)
    req = requests.get(url)
    with open(path, 'wb') as f:
       f.write(req.content)

def parse_cvrf(path, rel):

    r = os.path.dirname(path)
    headers={'User-Agent': random.choice(user_agent),
             'Referer': os.path.dirname(path),
            }

    req = requests.get(path, headers=headers)
    root = ET.fromstring(req.content)

    FIX = {}

    if not root.tag.endswith('cvrfdoc'):
        logging.error("Not a valid cvrf file, quit.")
        return None

    FIX['src_rpm'] = []
    FIX['rpm_x86'] = []
    FIX['rpm_aarch64'] = []
    FIX['rpm_noarch'] = []
    # remove namespace string
    for elem in root.getiterator():
        for n in ns:
            nstr = u'{%s}' % n
            l = len(nstr)
            if elem.tag.startswith(nstr):
                elem.tag = elem.tag[l:]

    for child in root.iter('ProductTree'):
        for b in child.findall('Branch'):
            if b.attrib['Type'] == 'Product Name' and b.attrib['Name'] == 'openEuler':
                vers = b.findall('FullProductName')
                target = False
                for ver in vers:
                    if ver.text == rel:
                        target = True
                        FIX['product'] = rel
                        break
                if target == False:
                    return None

            if b.attrib['Type'] == 'Package Arch' and b.attrib['Name'] == 'src':
                srcrpms = b.findall('FullProductName')
                for s in srcrpms:
                    FIX['src_rpm'].append(s.text)
            elif b.attrib['Type'] == 'Package Arch':
                rpms = b.findall('FullProductName')
                for r in rpms:
                    if r.text.endswith('.x86_64.rpm'):
                        FIX['rpm_x86'].append(r.text)
                    elif r.text.endswith('.aarch64.rpm'):
                        FIX['rpm_aarch64'].append(r.text)
                    else:
                        FIX['rpm_noarch'].append(r.text)

    FIX['items'] = []
    for child in root.iter('Vulnerability'):
        item = {}
        note = child.find('.//Notes/Note').text
        release_date = child.find('ReleaseDate').text
        cve = child.find('CVE').text
        status = child.find('.//ProductStatuses/Status').attrib['Type']
        threats = child.find('.//Threats/Threat/Description').text
        score = child.find('.//CVSSScoreSets/ScoreSet/BaseScore').text
        remedy = child.find('.//Remediations/Remediation/Description').text
        item['note'] = note
        item['release_date'] = release_date
        item['cve'] = cve
        item['status'] = status
        item['threats'] = threats
        item['score'] = score
        item['remedy'] = remedy

        FIX['items'].append(item)
    return FIX

def need_update(all_pkg, package, version, item):
    pkgs = all_pkg.filter(name=package)

    if len(pkgs) == 0:
        logging.error("pakckage %s not available in VesselOS 1.0 SP1" % package)
        return False

    for pkg in pkgs:
        ver = '%s-%s' % (pkg.version, pkg.release.replace('.vo1sp1', ''))
        logging.debug('available package: %s-%s\n' % (pkg.name, ver))
        if ver_parse(ver) < ver_parse(version):
            item['name'] = pkg.source_name
            item['from'] = ver
            item['to'] = version
            logging.debug("NEED UPDATE: %s : %s ==> %s" %(pkg.name, ver, version))
            return True
    return False

def get_log_level(level):
    if level == 'info':
        return logging.INFO
    elif level == 'debug':
        return logging.DEBUG
    elif level == 'error':
        return logging.ERROR

def query_pkg(base, pattern, tmpdir):
    q = base.sack.query()
    all_pkg = q.available()

    pkgs = all_pkg.filter(name__substr=pattern)

    if len(pkgs) == 0:
        print("No package match '%s'" % pattern)

    else:
        print("\nPakcages match '%s':\n" % pattern)
        for pkg in pkgs:
            print("%s: %s-%s-%s-%s" % (pkg.reponame, pkg.name, pkg.version, pkg.release, pkg.arch))

    shutil.rmtree(tmpdir)

def pkg_recorded(updates, item):
    for u in updates:
        if u['name'] == item['name']:
            return u

    return None

def main():
    parser = argparse.ArgumentParser(description='Parse cvrf from openEuler offical and print cve updates.')
    parser.add_argument('-r', '--release', dest='release', action='store', default='openEuler-22.03-LTS-SP1',
                        help='openEuler release targeted for')
    parser.add_argument('-p', '--period', dest='period', action='store', default="2023",
                        help='publish year, like 2022, 2023, default: 2023')

    parser.add_argument('--repo', dest='repository', action='append', default=['vesselos-1.0-sp1-everything', 'vesselos-1.0-sp1-updates'],
                        help='Vessel OS repository name, default: vesselos-1.0-sp1-everything')

    parser.add_argument('--baseurl', dest='baseurl', action='append',
                        default=['https://mirrors.jd.com/vesselos/VesselOS-1.0-SP1/everything/x86_64',
                                 'http://mirrors.jd.com/vesselos/VesselOS-1.0-SP1/updates/x86_64'],
                        help='Vessel OS repository baseurl, default: https://mirrors.jd.com/vesselos/VesselOS-1.0-SP1/everything/x86_64')

    parser.add_argument('-l', '--limit', dest='limit', action='store',
                        default=999999, type=int,
                        help='process max cve entries for target release and then quit')

    parser.add_argument('--log', dest='log_level', action='store',
                        default="info", choices=['info', 'debug', 'error'],
                        help='logging level selection')

    parser.add_argument('-s', '--search', dest='search_pattern', action='store',
                        default="",
                        help='search package from repos')

    parser.add_argument('--from', dest='sid_from', action='store', default="1001",
                        help='openEuler security advisory number parse from, default: 1001')

    parser.add_argument('--to', dest='sid_to', action='store', default="9999",
                        help='openEuler security advisory number parse ends, default: 9999 (up to current)')

    parser.add_argument('-d', '--detail', dest='show_detail', action='store_true', default=False,
                        help='show cve datils, default: False')

    args = parser.parse_args()
    default_release = args.release

    logging.basicConfig(level=get_log_level(args.log_level),
                        format='[%(asctime)s] %(levelname)s: %(message)s')

    if len(args.repository) != len(args.baseurl):
        logging.error("--repo must match --baseurl instance number")
        sys.exit(1)

    temp = tempfile.mkdtemp()

    base = dnf.Base()
    conf = base.conf
    conf.cachedir = temp

    rn = len(args.repository)
    for i in range(0, rn):
        base.repos.add_new_repo(args.repository[i], conf,
                            baseurl=[args.baseurl[i]])

    try:
        logging.info("initialize the repo metadata from %s ..." % args.baseurl)
        base.fill_sack(load_system_repo=False)
    except Exception as err:
        logging.error("repo init error: %s" % err)
        sys.exit(1)

    logging.debug('Enabled repositories:')
    for repo in base.repos.iter_enabled():
        logging.debug('id: %s' % repo.id)
        logging.debug('baseurl: %s' % repo.baseurl)

    # only query package
    if args.search_pattern != "":
        query_pkg(base, args.search_pattern, temp)
        sys.exit(0)

    q = base.sack.query()
    all_pkg = q.latest()

    logging.info("target release: %s" % default_release)
    logging.info("publish year: %s" % args.period)

    try:
        logging.info("download index file into %s" % temp)
        download(euler_cvrf_url + '/index.txt', temp)
    except Exception as err:
        logging.error("get index.txt failed: %s" % err)
        shutil.rmtree(temp)
        sys.exit(1)

    f = open(temp + '/index.txt', 'r')

    updates = []

    count = 0
    for sa in f.readlines():
        if sa.startswith(args.period):
            ret = sa.rfind('-')
            if ret == -1:
                logging.error("Invalid xml name!!!")
                continue

            sa_id = sa[ret +  1: ret + 5]

            if int(sa_id) < int(args.sid_from):
                continue

            if int(sa_id) > int(args.sid_to):
                break

            sa = sa.strip('\n')
            logging.info("Parsing SA: %s" % sa)
            try:
                result = parse_cvrf(euler_cvrf_url + r'/' + sa, default_release)
                if result != None:
                    updates.append(result)
                    count = count + 1
            except Exception as err:
                logging.error("parse cvrf: %s" % err)

            if count >= args.limit:
                break

    f.close()
    shutil.rmtree(temp)

    # print all updates
    logging.info("Get %d CVE update for %s\n" % (len(updates), default_release))

    UPDATE = []
    for u in updates:
        need = False
        # current we only support x86_64
        bin_rpms = u['rpm_x86'] + u['rpm_noarch']
        for s in bin_rpms:
            i = s.find('.oe2203sp1.')
            if i == -1:
                continue
            pkg = s[:i]
            logging.debug('update package: %s' % pkg)
            m = re.search('^.*(-[0-9]+(\.[0-9a-z]+)*-.*)', pkg)
            if m == None:
                logging.error("handle package %s error" % pkg)
                continue
            ver = m.group(1)
            i = pkg.find(ver)
            if i == -1:
                logging.error("version get failed, FIXME !!!")
                continue
            name = pkg[:i]
            need = need_update(all_pkg, name, ver[1:], u)
            # just one package is enough
            break

        if need == True:

            it = pkg_recorded(UPDATE, u)
            if it != None:
                it['items'].extend(u['items'])
            else:
                UPDATE.append(u)

            logging.debug('rpm: ', u['src_rpm'])
            for v in u['items']:
                logging.debug('CVE: %s' % v['cve'])
                logging.debug('release date: %s' % v['release_date'])

    print('\n')
    logging.info("Total update package: %d\n" % len(UPDATE))
    for i in UPDATE:
        cves = []
        for j in i['items']:
            cves.append(j['cve'])
        print("{}: {} -> {} ({})\n".format(i['name'], i['from'], i['to'], ','.join(cves)))

        if not args.show_detail:
            continue

        for j in i['items']:
            print("\t%s:" % j['cve'])
            print("\tNote: %s" % j['note'])
            print("\tRelease Date: %s" % j['release_date'])
            print("\tThreats: %s" % j['threats'])
            print("\tCVSS Score: %s" % j['score'])
            print("\n")

if __name__ == '__main__':
    main()
